﻿#include"XQFtpPI.h"
#include"XQFtp.h"
#include"XQFtpDTP.h"
#include"XQLog.hpp"
#include<QTcpSocket>
#include<QRegExp>
XQFtpPI::XQFtpPI(QObject* parent) :
    QObject(parent),
    rawCommand(false),
    transferConnectionExtended(true),
    m_dtp(new XQFtpDTP(this,this)),
    m_commandSocket(new QTcpSocket(this)),
    m_state(Begin), m_abortState(None),
    m_currentCmd(QString()),
    m_waitForDtpToConnect(false),
    m_waitForDtpToClose(false)
{
    m_commandSocket->setObjectName(QLatin1String("QFtpPI_socket"));
    connect(m_commandSocket, SIGNAL(hostFound()),
        SLOT(hostFound()));
    connect(m_commandSocket, SIGNAL(connected()),
        SLOT(connected()));
    connect(m_commandSocket, SIGNAL(disconnected()),
        SLOT(connectionClosed()));
    connect(m_commandSocket, SIGNAL(readyRead()),
        SLOT(readyRead()));
    connect(m_commandSocket,&QAbstractSocket::errorOccurred,
        this, QOverload<QAbstractSocket::SocketError>::of(&XQFtpPI::error));

    connect(m_dtp, SIGNAL(connectState(int)),
        SLOT(dtpConnectState(int)));
}

void XQFtpPI::connectToHost(const QString& host, quint16 port)
{
    emit connectState(Ftp::HostLookup);
#ifndef QT_NO_BEARERMANAGEMENT
    //将网络会话复制到套接字和DTP
    m_commandSocket->setProperty("_q_networksession", property("_q_networksession"));
    m_dtp->setProperty("_q_networksession", property("_q_networksession"));
#endif
    m_commandSocket->connectToHost(host, port);
    if (isDebugModel(Ftp::Connection))
        XQFtpLog << "连接服务器:" << host << ":" << port;
}

/*
  Sends the sequence of commands \a cmds to the FTP server. When the commands
  are all done the finished() signal is emitted. When an error occurs, the
  error() signal is emitted.

  If there are pending commands in the queue this functions returns false and
  the \a cmds are not added to the queue; otherwise it returns true.
*/
bool XQFtpPI::sendCommands(const QStringList& cmds)
{
    if (!m_pendingCommands.isEmpty())
        return false;

    if (m_commandSocket->state() != QTcpSocket::ConnectedState || m_state != Idle) {
        emit error(Ftp::NotConnected, XQFtp::tr("Not connected"));
        return true; // there are no pending commands
    }

    m_pendingCommands = cmds;
    startNextCmd();
    return true;
}

bool XQFtpPI::sendCommand(const QString& cmd)
{
    return sendCommands(QStringList(cmd));
}

void XQFtpPI::clearPendingCommands()
{
    m_pendingCommands.clear();
    m_dtp->abortConnection();
    m_currentCmd.clear();
    m_state = Idle;
}

void XQFtpPI::abort()
{
    m_pendingCommands.clear();

    if (m_abortState != None)
        // ABOR already sent
        return;

    m_abortState = AbortStarted;
#if defined(QFTPPI_DEBUG)
    qDebug("QFtpPI send: ABOR");
#endif
    m_commandSocket->write("ABOR\r\n", 6);

    if (m_currentCmd.startsWith(QLatin1String("STOR ")))
        m_dtp->abortConnection();
}

void XQFtpPI::setDebug(int* debug)
{
    m_debug = debug;
    if(m_dtp)
        m_dtp->setDebug(debug);
}

bool XQFtpPI::isDebugModel(Ftp::Debug model)
{
    if (m_debug == nullptr)
        return false;
    return  (*m_debug)&model;
}

QString XQFtpPI::currentCommand() const
{
    return m_currentCmd;
}

void XQFtpPI::hostFound()
{
    emit connectState(Ftp::Connecting);
}

void XQFtpPI::connected()
{
    m_state = Begin;
#if defined(QFTPPI_DEBUG)
    //    qDebug("QFtpPI state: %d [connected()]", state);
#endif
    // try to improve performance by setting TCP_NODELAY
    m_commandSocket->setSocketOption(QAbstractSocket::LowDelayOption, 1);

    emit connectState(Ftp::Connected);
}

void XQFtpPI::connectionClosed()
{
    m_commandSocket->close();
    emit connectState(Ftp::Unconnected);
}

void XQFtpPI::delayedCloseFinished()
{
    emit connectState(Ftp::Unconnected);
}

void XQFtpPI::error(QAbstractSocket::SocketError e)
{
    if (e == QTcpSocket::HostNotFoundError) {
        emit connectState(Ftp::Unconnected);
        emit error(Ftp::HostNotFound,
            XQFtp::tr("Host %1 not found").arg(m_commandSocket->peerName()));
    }
    else if (e == QTcpSocket::ConnectionRefusedError) {
        emit connectState(Ftp::Unconnected);
        emit error(Ftp::ConnectionRefused,
            XQFtp::tr("Connection refused to host %1").arg(m_commandSocket->peerName()));
    }
    else if (e == QTcpSocket::SocketTimeoutError) {
        emit connectState(Ftp::Unconnected);
        emit error(Ftp::ConnectionRefused,
            XQFtp::tr("Connection timed out to host %1").arg(m_commandSocket->peerName()));
    }
}

void XQFtpPI::readyRead()
{
    if (m_waitForDtpToClose)
        return;

    while (m_commandSocket->canReadLine()) {
        // read line with respect to line continuation
        QString line = QString::fromLatin1(m_commandSocket->readLine());
        if (m_replyText.isEmpty()) {
            if (line.length() < 3) {
                // protocol error
                return;
            }
            const int lowerLimit[3] = { 1,0,0 };
            const int upperLimit[3] = { 5,5,9 };
            for (int i = 0; i < 3; i++) {
                m_replyCode[i] = line[i].digitValue();
                if (m_replyCode[i]<lowerLimit[i] || m_replyCode[i]>upperLimit[i]) {
                    // protocol error
                    return;
                }
            }
        }
        QString endOfMultiLine = QString::number(m_replyCode[0]) + QString::number(m_replyCode[1]) + QString::number(m_replyCode[2]) + QLatin1Char(' ');
        QString lineCont(endOfMultiLine);
        lineCont[3] = QLatin1Char('-');
        QString lineLeft4 = line.left(4);

        while (lineLeft4 != endOfMultiLine) {
            if (lineLeft4 == lineCont)
                m_replyText += line.mid(4); // strip 'xyz-'
            else
                m_replyText += line;
            if (!m_commandSocket->canReadLine())
                return;
            line = QString::fromLatin1(m_commandSocket->readLine());
            lineLeft4 = line.left(4);
        }
        m_replyText += line.mid(4); // strip reply code 'xyz '
        if (m_replyText.endsWith(QLatin1String("\r\n")))
            m_replyText.chop(2);
        if (isDebugModel(Ftp::CommandRead))
            XQFtpLog << "返回信息:" << QString("%1%2%3 ").arg(m_replyCode[0] * 1).arg(m_replyCode[1] * 1).arg(m_replyCode[2] * 1) + m_replyText.trimmed();
        if (processReply())
            m_replyText = QLatin1String("");
    }
    
}

/*
  Process a reply from the FTP server.

  Returns true if the reply was processed or false if the reply has to be
  processed at a later point.
*/
bool XQFtpPI::processReply()
{
#if defined(QFTPPI_DEBUG)
    //    qDebug("QFtpPI state: %d [processReply() begin]", state);
    if (replyText.length() < 400)
        qDebug("QFtpPI recv: %d %s", 100 * replyCode[0] + 10 * replyCode[1] + replyCode[2], replyText.toLatin1().constData());
    else
        qDebug("QFtpPI recv: %d (text skipped)", 100 * replyCode[0] + 10 * replyCode[1] + replyCode[2]);
#endif

    int replyCodeInt = 100 * m_replyCode[0] + 10 * m_replyCode[1] + m_replyCode[2];

    // process 226 replies ("Closing Data Connection") only when the data
    // connection is really closed to avoid short reads of the DTP
    if (replyCodeInt == 226 || (replyCodeInt == 250 && m_currentCmd.startsWith(QLatin1String("RETR")))) {
        if (m_dtp->state() != QTcpSocket::UnconnectedState) {
            m_waitForDtpToClose = true;
            return false;
        }
    }

    switch (m_abortState) {
    case AbortStarted:
        m_abortState = WaitForAbortToFinish;
        break;
    case WaitForAbortToFinish:
        m_abortState = None;
        return true;
    default:
        break;
    }

    // get new state
    static const State table[5] = {
        /* 1yz   2yz      3yz   4yz      5yz */
        Waiting, Success, Idle, Failure, Failure
    };
    switch (m_state) {
    case Begin:
        if (m_replyCode[0] == 1) {
            return true;
        }
        else if (m_replyCode[0] == 2) {
            m_state = Idle;
            emit finished(XQFtp::tr("Connected to host %1").arg(m_commandSocket->peerName()));
            break;
        }
        // reply codes not starting with 1 or 2 are not handled.
        return true;
    case Waiting:
        if (static_cast<signed char>(m_replyCode[0]) < 0 || m_replyCode[0] > 5)
            m_state = Failure;
        else
#if defined(Q_OS_IRIX) && defined(Q_CC_GNU)
        {
            // work around a crash on 64 bit gcc IRIX
            State* t = (State*)table;
            state = t[replyCode[0] - 1];
        }
#else
            if (replyCodeInt == 202)
                m_state = Failure;
            else
                m_state = table[m_replyCode[0] - 1];
#endif
        break;
    default:
        // ignore unrequested message
        return true;
    }
#if defined(QFTPPI_DEBUG)
    //    qDebug("QFtpPI state: %d [processReply() intermediate]", state);
#endif

    // special actions on certain replies
    emit rawFtpReply(replyCodeInt, m_replyText);
    if (rawCommand) {
        rawCommand = false;
    }
    else if (replyCodeInt == 227) {
        // 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
        // rfc959 does not define this response precisely, and gives
        // both examples where the parenthesis are used, and where
        // they are missing. We need to scan for the address and host
        // info.
        QRegExp addrPortPattern(QLatin1String("(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+)"));
        if (addrPortPattern.indexIn(m_replyText) == -1) {
#if defined(QFTPPI_DEBUG)
            qDebug("QFtp: bad 227 response -- address and port information missing");
#endif
            // this error should be reported
        }
        else {
            QStringList lst = addrPortPattern.capturedTexts();
            QString host = lst[1] + QLatin1Char('.') + lst[2] + QLatin1Char('.') + lst[3] + QLatin1Char('.') + lst[4];
            quint16 port = (lst[5].toUInt() << 8) + lst[6].toUInt();
            m_waitForDtpToConnect = true;
            m_dtp->connectToHost(host, port);
        }
    }
    else if (replyCodeInt == 229) {
        // 229 Extended Passive mode OK (|||10982|)
        int portPos = m_replyText.indexOf(QLatin1Char('('));
        if (portPos == -1) {
#if defined(QFTPPI_DEBUG)
            qDebug("QFtp: bad 229 response -- port information missing");
#endif
            // this error should be reported
        }
        else {
            ++portPos;
            QChar delimiter = m_replyText.at(portPos);
            QStringList epsvParameters = m_replyText.mid(portPos).split(delimiter);

            m_waitForDtpToConnect = true;
            m_dtp->connectToHost(m_commandSocket->peerAddress().toString(),
                epsvParameters.at(3).toInt());
        }

    }
    else if (replyCodeInt == 230) {
        if (m_currentCmd.startsWith(QLatin1String("USER ")) && m_pendingCommands.count() > 0 &&
            m_pendingCommands.first().startsWith(QLatin1String("PASS "))) {
            // no need to send the PASS -- we are already logged in
            m_pendingCommands.pop_front();
        }
        // 230 User logged in, proceed.
        emit connectState(Ftp::LoggedIn);
    }
    else if (replyCodeInt == 213) {
        // 213 File status.
        if (m_currentCmd.startsWith(QLatin1String("SIZE ")))
            m_dtp->setBytesTotal(m_replyText.simplified().toLongLong());
    }
    else if (m_replyCode[0] == 1 && m_currentCmd.startsWith(QLatin1String("STOR "))) {
        m_dtp->waitForConnection();
        m_dtp->writeData();
    }

    // react on new state
    switch (m_state) {
    case Begin:
        // should never happen
        break;
    case Success:
        // success handling
        m_state = Idle;
        // no break!
    case Idle:
        if (m_dtp->hasError()) {
            emit error(Ftp::UnknownError, m_dtp->errorMessage());
            m_dtp->clearError();
        }
        startNextCmd();
        break;
    case Waiting:
        // do nothing
        break;
    case Failure:
        // If the EPSV or EPRT commands fail, replace them with
        // the old PASV and PORT instead and try again.
        if (m_currentCmd.startsWith(QLatin1String("EPSV"))) {
            transferConnectionExtended = false;
            m_pendingCommands.prepend(QLatin1String("PASV\r\n"));
        }
        else if (m_currentCmd.startsWith(QLatin1String("EPRT"))) {
            transferConnectionExtended = false;
            m_pendingCommands.prepend(QLatin1String("PORT\r\n"));
        }
        else {
            emit error(Ftp::UnknownError, m_replyText);
        }
        if (m_state != Waiting) {
            m_state = Idle;
            startNextCmd();
        }
        break;
    }
#if defined(QFTPPI_DEBUG)
    //    qDebug("QFtpPI state: %d [processReply() end]", state);
#endif
    return true;
}

/*
  Starts next pending command. Returns false if there are no pending commands,
  otherwise it returns true.
*/
bool XQFtpPI::startNextCmd()
{
    if (m_waitForDtpToConnect)
        // don't process any new commands until we are connected
        return true;

#if defined(QFTPPI_DEBUG)
    if (state != Idle)
        qDebug("QFtpPI startNextCmd: Internal error! QFtpPI called in non-Idle state %d", state);
#endif
    if (m_pendingCommands.isEmpty()) {
        m_currentCmd.clear();
        emit finished(m_replyText);
        return false;
    }
    m_currentCmd = m_pendingCommands.first();

    // PORT and PASV are edited in-place, depending on whether we
    // should try the extended transfer connection commands EPRT and
    // EPSV. The PORT command also triggers setting up a listener, and
    // the address/port arguments are edited in.
    QHostAddress address = m_commandSocket->localAddress();
    if (m_currentCmd.startsWith(QLatin1String("PORT"))) {
        if ((address.protocol() == QTcpSocket::IPv6Protocol) && transferConnectionExtended) {
            int port = m_dtp->setupListener(address);
            m_currentCmd = QLatin1String("EPRT |");
            m_currentCmd += (address.protocol() == QTcpSocket::IPv4Protocol) ? QLatin1Char('1') : QLatin1Char('2');
            m_currentCmd += QLatin1Char('|') + address.toString() + QLatin1Char('|') + QString::number(port);
            m_currentCmd += QLatin1Char('|');
        }
        else if (address.protocol() == QTcpSocket::IPv4Protocol) {
            int port = m_dtp->setupListener(address);
            QString portArg;
            quint32 ip = address.toIPv4Address();
            portArg += QString::number((ip & 0xff000000) >> 24);
            portArg += QLatin1Char(',') + QString::number((ip & 0xff0000) >> 16);
            portArg += QLatin1Char(',') + QString::number((ip & 0xff00) >> 8);
            portArg += QLatin1Char(',') + QString::number(ip & 0xff);
            portArg += QLatin1Char(',') + QString::number((port & 0xff00) >> 8);
            portArg += QLatin1Char(',') + QString::number(port & 0xff);

            m_currentCmd = QLatin1String("PORT ");
            m_currentCmd += portArg;
        }
        else {
            // No IPv6 connection can be set up with the PORT
            // command.
            return false;
        }

        m_currentCmd += QLatin1String("\r\n");
    }
    else if (m_currentCmd.startsWith(QLatin1String("PASV"))) {
        if ((address.protocol() == QTcpSocket::IPv6Protocol) && transferConnectionExtended)
            m_currentCmd = QLatin1String("EPSV\r\n");
    }

    m_pendingCommands.pop_front();
#if defined(QFTPPI_DEBUG)
    qDebug("QFtpPI send: %s", currentCmd.left(currentCmd.length() - 2).toLatin1().constData());
#endif
    m_state = Waiting;
    auto sendData = m_currentCmd.toLatin1();
    if(isDebugModel(Ftp::CommandWrite))
        XQFtpLog <<"发送命令:"<< sendData.trimmed();
    m_commandSocket->write(std::move(sendData));
    return true;
}

void XQFtpPI::dtpConnectState(int s)
{
    switch (s) {
    case XQFtpDTP::CsClosed:
        if (m_waitForDtpToClose) {
            // there is an unprocessed reply
            if (processReply())
                m_replyText = QLatin1String("");
            else
                return;
        }
        m_waitForDtpToClose = false;
        readyRead();
        return;
    case XQFtpDTP::CsConnected:
        m_waitForDtpToConnect = false;
        startNextCmd();
        return;
    case XQFtpDTP::CsHostNotFound:
    case XQFtpDTP::CsConnectionRefused:
        emit error(Ftp::ConnectionRefused,
            XQFtp::tr("Connection refused for data connection"));
        startNextCmd();
        return;
    default:
        return;
    }
}